﻿using UnityEngine;
using System.Collections;
namespace LightShafts
{
    public enum LightShaftsShadowmapMode
    {
        Dynamic = 0,
        Static = 1
    }

    public partial class LightShafts : MonoBehaviour
    {
        public bool directional { get { return m_LightType == LightType.Directional; } }
        public bool spot { get { return m_LightType == LightType.Spot; } }

        Bounds GetBoundsLocal()
        {
            if (directional)
                return new Bounds(new Vector3(0, 0, m_Size.z * 0.5f), m_Size);

            Light l = m_Light;
            Vector3 offset = new Vector3(0, 0, l.range * (m_SpotFar + m_SpotNear) * 0.5f);
            float height = (m_SpotFar - m_SpotNear) * l.range;
            float baseSize = Mathf.Tan(l.spotAngle * Mathf.Deg2Rad * 0.5f) * m_SpotFar * l.range * 2.0f;
            return new Bounds(offset, new Vector3(baseSize, baseSize, height));
        }

        Matrix4x4 GetBoundsMatrix()
        {
            Bounds bounds = GetBoundsLocal();
            Transform t = transform;
            return Matrix4x4.TRS(t.position + t.forward * bounds.center.z, t.rotation, bounds.size);
        }

        float GetFrustumApex()
        {
            // Assuming the frustum is inscribed in a unit cube centered at 0
            return -m_SpotNear / (m_SpotFar - m_SpotNear) - 0.5f;
        }

        void OnDrawGizmosSelected()
        {
            UpdateLightType();

            Gizmos.color = Color.yellow;
            if (directional)
            {
                Gizmos.matrix = GetBoundsMatrix();
                Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
            }
            else if (spot)
            {
                Transform t = transform;
                Light l = m_Light;
                Gizmos.matrix = Matrix4x4.TRS(t.position, t.rotation, Vector3.one);
                Gizmos.DrawFrustum(t.position, l.spotAngle, l.range * m_SpotFar, l.range * m_SpotNear, 1);
            }
        }

        void RenderQuadSections(Vector4 lightPos)
        {
            for (int i = 0; i < 4; i++)
            {
                // Skip one or two quarters, if the light is off screen
                if (i == 0 && lightPos.y > 1 ||
                    i == 1 && lightPos.x > 1 ||
                    i == 2 && lightPos.y < -1 ||
                    i == 3 && lightPos.x < -1)
                    continue;

                // index denotes which quarter of the screen to take up,
                // so start at -1, -0.5, 0 or 0.5
                float top = i / 2.0f - 1.0f;
                float bottom = top + 0.5f;
                GL.Begin(GL.QUADS);
                GL.Vertex3(-1, top, 0);
                GL.Vertex3(1, top, 0);
                GL.Vertex3(1, bottom, 0);
                GL.Vertex3(-1, bottom, 0);
                GL.End();
            }
        }

        void RenderQuad()
        {
            GL.Begin(GL.QUADS);
            GL.TexCoord2(0, 0);
            GL.Vertex3(-1, -1, 0);
            GL.TexCoord2(0, 1);
            GL.Vertex3(-1, 1, 0);
            GL.TexCoord2(1, 1);
            GL.Vertex3(1, 1, 0);
            GL.TexCoord2(1, 0);
            GL.Vertex3(1, -1, 0);
            GL.End();
        }

        void RenderSpotFrustum()
        {
            Graphics.DrawMeshNow(m_SpotMesh, transform.position, transform.rotation);
        }

        Vector4 GetLightViewportPos()
        {
            Vector3 lightPos = transform.position;
            if (directional)
                lightPos = m_CurrentCamera.transform.position + transform.forward;

            Vector3 lightViewportPos3 = m_CurrentCamera.WorldToViewportPoint(lightPos);
            return new Vector4(lightViewportPos3.x * 2.0f - 1.0f, lightViewportPos3.y * 2.0f - 1.0f, 0, 0);
        }

        bool IsVisible()
        {
            // Intersect against spot light's OBB (or light frustum's OBB), so AABB in it's space
            Matrix4x4 lightToCameraProjection = m_CurrentCamera.projectionMatrix * m_CurrentCamera.worldToCameraMatrix * transform.localToWorldMatrix;
            return GeometryUtility.TestPlanesAABB(GeometryUtility.CalculateFrustumPlanes(lightToCameraProjection), GetBoundsLocal());
        }

        bool IntersectsNearPlane()
        {
            // Lazy for now:
            // Just check if any vertex is behind the near plane.
            // TODO: same for directional
            Vector3[] vertices = m_SpotMesh.vertices;
            float nearPlaneFudged = m_CurrentCamera.nearClipPlane - 0.001f;
            Transform t = transform;
            for (int i = 0; i < vertices.Length; i++)
            {
                float z = m_CurrentCamera.WorldToViewportPoint(t.TransformPoint(vertices[i])).z;
                if (z < nearPlaneFudged)
                    return true;
            }
            return false;
        }

        void SetKeyword(bool firstOn, string firstKeyword, string secondKeyword)
        {
            Shader.EnableKeyword(firstOn ? firstKeyword : secondKeyword);
            Shader.DisableKeyword(firstOn ? secondKeyword : firstKeyword);
        }

        public void SetShadowmapDirty()
        {
            m_ShadowmapDirty = true;
        }

        void GetFrustumRays(out Matrix4x4 frustumRays, out Vector3 cameraPosLocal)
        {
            float far = m_CurrentCamera.farClipPlane;
            Vector3 cameraPos = m_CurrentCamera.transform.position;
            Matrix4x4 m = GetBoundsMatrix().inverse;
            Vector2[] uvs = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) };
            frustumRays = new Matrix4x4();

            for (int i = 0; i < 4; i++)
            {
                Vector3 ray = m_CurrentCamera.ViewportToWorldPoint(new Vector3(uvs[i].x, uvs[i].y, far)) - cameraPos;
                ray = m.MultiplyVector(ray);
                frustumRays.SetRow(i, ray);
            }

            cameraPosLocal = m.MultiplyPoint3x4(cameraPos);
        }

        void SetFrustumRays(Material material)
        {
            Matrix4x4 frustumRays;
            Vector3 cameraPosLocal;
            GetFrustumRays(out frustumRays, out cameraPosLocal);
            material.SetVector("_CameraPosLocal", cameraPosLocal);
            material.SetMatrix("_FrustumRays", frustumRays);
            material.SetFloat("_FrustumApex", GetFrustumApex());
        }

        float GetDepthThresholdAdjusted()
        {
            return m_DepthThreshold / m_CurrentCamera.farClipPlane;
        }

        bool CheckCamera()
        {
            if (m_Cameras == null)
                return false;

            foreach (Camera cam in m_Cameras)
                if (cam == m_CurrentCamera)
                    return true;

            return false;
        }

        public void UpdateCameraDepthMode()
        {
            if (m_Cameras == null)
                return;

            foreach (Camera cam in m_Cameras)
                if (cam)
                    cam.depthTextureMode |= DepthTextureMode.Depth;
        }
    }
}